﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Microsoft.DotNet.Cli.Commands.Test.IPC.Models;

namespace Microsoft.DotNet.Cli.Commands.Test.IPC.Serializers;

/*
|---FieldCount---| 2 bytes

|---ModuleName Id---| (2 bytes)
|---ModuleName Size---| (4 bytes)
|---ModuleName Value---| (n bytes)

|---CommandLineOptionMessageList Id---| (2 bytes)
|---CommandLineOptionMessageList Size---| (4 bytes)
|---CommandLineOptionMessageList Value---| (n bytes)
    |---CommandLineOptionMessageList Length---| (4 bytes)

    |---CommandLineOptionMessageList[0] FieldCount---| 2 bytes

    |---CommandLineOptionMessageList[0].Name Id---| (2 bytes)
    |---CommandLineOptionMessageList[0].Name Size---| (4 bytes)
    |---CommandLineOptionMessageList[0].Name Value---| (n bytes)

    |---CommandLineOptionMessageList[0].Description Id---| (2 bytes)
    |---CommandLineOptionMessageList[0].Description Size---| (4 bytes)
    |---CommandLineOptionMessageList[0].Description Value---| (n bytes)

    |---CommandLineOptionMessageList[0].IsHidden Id---| (2 bytes)
    |---CommandLineOptionMessageList[0].IsHidden Size---| (4 bytes)
    |---CommandLineOptionMessageList[0].IsHidden Value---| (1 byte)

    |---CommandLineOptionMessageList[0].IsBuiltIn Id---| (2 bytes)
    |---CommandLineOptionMessageList[0].IsBuiltIn Size---| (4 bytes)
    |---CommandLineOptionMessageList[0].IsBuiltIn Value---| (1 byte)
*/

internal sealed class CommandLineOptionMessagesSerializer : BaseSerializer, INamedPipeSerializer
{
    public int Id => CommandLineOptionMessagesFieldsId.MessagesSerializerId;

    public object Deserialize(Stream stream)
    {
        string? moduleName = null;
        List<CommandLineOptionMessage>? commandLineOptionMessages = null;

        ushort fieldCount = ReadUShort(stream);

        for (int i = 0; i < fieldCount; i++)
        {
            int fieldId = ReadUShort(stream);
            int fieldSize = ReadInt(stream);

            switch (fieldId)
            {
                case CommandLineOptionMessagesFieldsId.ModulePath:
                    moduleName = ReadStringValue(stream, fieldSize);
                    break;

                case CommandLineOptionMessagesFieldsId.CommandLineOptionMessageList:
                    commandLineOptionMessages = ReadCommandLineOptionMessagesPayload(stream);
                    break;

                default:
                    // If we don't recognize the field id, skip the payload corresponding to that field
                    SetPosition(stream, stream.Position + fieldSize);
                    break;
            }
        }

        return new CommandLineOptionMessages(moduleName, commandLineOptionMessages is null ? [] : [.. commandLineOptionMessages]);
    }

    private static List<CommandLineOptionMessage> ReadCommandLineOptionMessagesPayload(Stream stream)
    {
        List<CommandLineOptionMessage> commandLineOptionMessages = [];

        int length = ReadInt(stream);
        for (int i = 0; i < length; i++)
        {
            string? name = null, description = null;
            bool? isHidden = null, isBuiltIn = null;

            int fieldCount = ReadUShort(stream);

            for (int j = 0; j < fieldCount; j++)
            {
                int fieldId = ReadUShort(stream);
                int fieldSize = ReadInt(stream);

                switch (fieldId)
                {
                    case CommandLineOptionMessageFieldsId.Name:
                        name = ReadStringValue(stream, fieldSize);
                        break;

                    case CommandLineOptionMessageFieldsId.Description:
                        description = ReadStringValue(stream, fieldSize);
                        break;

                    case CommandLineOptionMessageFieldsId.IsHidden:
                        isHidden = ReadBool(stream);
                        break;

                    case CommandLineOptionMessageFieldsId.IsBuiltIn:
                        isBuiltIn = ReadBool(stream);
                        break;

                    default:
                        SetPosition(stream, stream.Position + fieldSize);
                        break;
                }
            }

            commandLineOptionMessages.Add(new CommandLineOptionMessage(name, description, isHidden, isBuiltIn));
        }

        return commandLineOptionMessages;
    }

    public void Serialize(object objectToSerialize, Stream stream)
    {
        Debug.Assert(stream.CanSeek, "We expect a seekable stream.");

        var commandLineOptionMessages = (CommandLineOptionMessages)objectToSerialize;

        WriteUShort(stream, GetFieldCount(commandLineOptionMessages));

        WriteField(stream, CommandLineOptionMessagesFieldsId.ModulePath, commandLineOptionMessages.ModulePath);
        WriteCommandLineOptionMessagesPayload(stream, commandLineOptionMessages.CommandLineOptionMessageList);
    }

    private static void WriteCommandLineOptionMessagesPayload(Stream stream, CommandLineOptionMessage[]? commandLineOptionMessageList)
    {
        if (commandLineOptionMessageList is null || commandLineOptionMessageList.Length == 0)
        {
            return;
        }

        WriteUShort(stream, CommandLineOptionMessagesFieldsId.CommandLineOptionMessageList);

        // We will reserve an int (4 bytes)
        // so that we fill the size later, once we write the payload
        WriteInt(stream, 0);

        long before = stream.Position;
        WriteInt(stream, commandLineOptionMessageList.Length);
        foreach (CommandLineOptionMessage commandLineOptionMessage in commandLineOptionMessageList)
        {
            WriteUShort(stream, GetFieldCount(commandLineOptionMessage));

            WriteField(stream, CommandLineOptionMessageFieldsId.Name, commandLineOptionMessage.Name);
            WriteField(stream, CommandLineOptionMessageFieldsId.Description, commandLineOptionMessage.Description);
            WriteField(stream, CommandLineOptionMessageFieldsId.IsHidden, commandLineOptionMessage.IsHidden);
            WriteField(stream, CommandLineOptionMessageFieldsId.IsBuiltIn, commandLineOptionMessage.IsBuiltIn);
        }

        // NOTE: We are able to seek only if we are using a MemoryStream
        // thus, the seek operation is fast as we are only changing the value of a property
        WriteAtPosition(stream, (int)(stream.Position - before), before - sizeof(int));
    }

    private static ushort GetFieldCount(CommandLineOptionMessages commandLineOptionMessages) =>
        (ushort)((commandLineOptionMessages.ModulePath is null ? 0 : 1) +
        (IsNullOrEmpty(commandLineOptionMessages.CommandLineOptionMessageList) ? 0 : 1));

    private static ushort GetFieldCount(CommandLineOptionMessage commandLineOptionMessage) =>
        (ushort)((commandLineOptionMessage.Name is null ? 0 : 1) +
        (commandLineOptionMessage.Description is null ? 0 : 1) +
        (commandLineOptionMessage.IsHidden is null ? 0 : 1) +
        (commandLineOptionMessage.IsBuiltIn is null ? 0 : 1));
}
